使用线程池

作者:[美]易格恩.阿格佛温(Eugene Agafonov)
译者:黄博文 黄辉兰
改编:陈广
日期:2018-3-17


简介

在之前的章节中我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。

为了解决该问题,有一个常用的方式叫做(pooling)。线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用时,就将其返回到池中。

.NET线程池是该概念的一种实现。通过System.Threading.ThreadPool类型可以使用线程池。线程池是受 .NET通用语言运行时(Common Language Runtime,简称CLR)管理的。这意味着每个CLR都有一个线程池实例。ThreadPool类型拥有一个QueueUserWorkItem静态方法。该静态方法接受一个委托,代表用户自定义的一个异步操作。在该方法被调用后,委托会进入到内部队列中。如果池中没有任何线程,将创建一个新的工作线程(worker thread),并将队列中第一个委托放入到该工作线程中。

如果想线程池中放入新的操作,当之前的所有操作完成后,很可能只需重用一个线程来执行这些新的操作,当之前的所有操作完成后,很可能只需重用一个线程来执行这些新的操作。然而,如果放置新的操作过快,线程池将创建更多的线程来执行这些操作。创建线程的数量是有限制的,在这种情况下新的操作将在队列中等待直到线程池中的工作线程有能力来执行它们。

提示: 保持线程中的操作都是短暂的是非常重要的。不要在线程池中放入长时间运行的操作,或者阻塞工作线程。这将导致所有工作线程变得繁忙,从而无法服务用户操作。这会导致性能问题和非常难以调试的错误。

当停止向线程池中放置新操作时,线程池最终会删除一定时后过期的不再使用的线程。这将释放所有那些不再需要的系统资源。

再次强调,线程池的用途是执行运行时间短的操作。使用线程池可以减少并行度耗费及节省操作系统资源。我们只使用较少的线程,但是以比平常的速度来执行异步操作,使用一定数量的可用的工作线程批量处理这些操作。如果操作能快速地完成则比较适用线程池,但是执行长时间运行的计算密集型操作则会降低性能。

另一个重要事情是在ASP.NET应用程序中使用线程池时要相当小心。ASP.NET基础设施使用自己的线程池,如果在线程池中浪费所有的工作线程,Web服务器将不能够服务新的请求。在ASP.NET中只推荐使用输入/输出密集型的异步操作,因为其使用了一个不同的方式,叫做I/O线程。我们将在之后讨论I/O线程。

注意: 线程池中的工作线程都是后台线程。这意味着当所有的前台线程(包括主程序线程)完成后,所有的后台线程将停止工作。

在本章中,我们将学习使用线程池来执行异步操作。本章将覆盖将操作放入线程池的不同方式,以及如何取消一个操作,并防止其长时间运行。

在线程池中调用委托

本节将展示在线程池中如何异步地执行委托。另外,我们将讨论一个叫做异步编程模型(Asynchronous Programming Model,简称APM)的方式,这是.NET历史中第一个异步编程模式。需要注意,.NET Core已经不再支持使用这种方式进行异步编程。需要在Visual Studio里使用.NET Framework的控制台程序来编译运行以下代码。

private delegate string RunOnThreadPool(out int threadId);
static void Main(string[] args)
{
    int threadId = 0;
    RunOnThreadPool poolDelegate = Test;

    var t = new Thread(() => Test(out threadId));
    t.Start();
    t.Join();
    Console.WriteLine("线程 id: {0}", threadId);

    IAsyncResult r = poolDelegate.BeginInvoke(out threadId,
        Callback, "委托异步调用");
    r.AsyncWaitHandle.WaitOne();

    string result = poolDelegate.EndInvoke(out threadId, r);
    Console.WriteLine("线程池工作线程ID: {0}", threadId);
    Console.WriteLine(result);
    Thread.Sleep(TimeSpan.FromSeconds(2));
    Console.ReadLine();
}

private static void Callback(IAsyncResult ar)
{
    Console.WriteLine("开始执行回调函数...");
    Console.WriteLine("传递给回调函数的state: {0}", ar.AsyncState);
    Console.WriteLine("是否线程池中的线程: {0}",
    Thread.CurrentThread.IsThreadPoolThread);
    Console.WriteLine("线程池工作线程ID: {0}",
    Thread.CurrentThread.ManagedThreadId);
}

private static string Test(out int threadId)
{
    Console.WriteLine("Starting...");
    Console.WriteLine("是否线程池中的线程: {0}",
    Thread.CurrentThread.IsThreadPoolThread);
    Thread.Sleep(TimeSpan.FromSeconds(2));
    threadId = Thread.CurrentThread.ManagedThreadId;
    return string.Format("线程池工作线程ID: {0}", threadId);
}

运行结果:

Test:启动...
Test:是否线程池中的线程: False
Main:线程 id: 3
Test:启动...
Test:是否线程池中的线程: True
Main:线程池工作线程ID: 4
Main:Test:线程池工作线程ID: 4
Callback:开始执行回调函数...
Callback:传递给回调函数的state: 委托异步调用
Callback:是否线程池中的线程: True
Callback:线程池工作线程ID: 4

说实在的,这种编程方式挺烧脑。微软给出的网络编程示例也是类似这种方式,更加烧脑、复杂、麻烦。当然这种方式已经淘汰,后面就舒服多了。

在此示例中,线程的第一次启动是通过传统方式创建的,从结果可知,它并没有使用线程池。线程的第二次启动通过调用方法所对应委托的BeginInvoke方法来进行的,它启用了线程池。线程工作完毕后自动调用被作为参数的回调函数Callback

当需要异步操作的结果时,可以使用BeginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但在本例中,使用的是AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。

提示: 事实上使用AsyncWaitHandle并不是必要的。如果注释掉r.AsyncWaitHandle.WaitOne(),代码照样可以成功运行,因为EndInvoke方法事实上会等待异步操作完成。调用EndInvoke方法(或者针对其他异步API的EndOperationName方法)是非常重要的,因为该方法会将任何未处理的异常抛回到调用线程中。当使用这种异步API时,请确保始终调用了BeginEnd方法。

当操作完成后,传递给BeginInvoke方法的回调函数将被放置到线程池中,确切地说是一个工作线程中。如果在Main方法定义的结尾注释掉Thread.Sleep方法调用,回调函数将不会被执行。这是因为当主线程完成后,所有的后台线程都被停止,包括该回调函数。对委托和回调函数的异步调用很可能会被同一个工作线程执行。通过工作线程ID可以容易地看出。

使用BeginOperationName/EndOperationName方法和.NET中的`IAsyncResult对象等方式被称为异步编程模型(或APM模式),这样的方法对被称为异步方法。该模式也被应用于多个.NET类库的API中,但在现代编程中,更推荐使用任务并行库(Task Parallel Library,简称TPL)来组织异步API。之后会讨论。

多线程访问UI

多线程访问UI可以说是Windows应用程序编程的一门必修课。上述例子正好使用Visual Studio编程,在这里顺便就把这个话题给讲了。

打开Visual Studio,新建一个Windows应用程序(vs2017中是Windows窗体应用)项目。在工具箱所有Windows窗体栏中拖一个ProgressBar控件到窗体上。然后再拖动一个按钮控件到窗体上。界面如下图所示:

在主线程更新ProgressBar

双击按钮生成button1_Click事件,在代码窗口输入如下代码:

private void button1_Click(object sender, EventArgs e)
{
    int percent = 0;
    while (percent <= 100)
    {
        progressBar1.Value = percent;
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        percent += 5;
    }
}

运行程序,点击按钮,程序正常运行,但这时如果用鼠标试图拖动窗体,是没办法移动窗体的。你在进度条更新期间无法做任何事,因为所有行为都在主线程发生,刷新进度条当然就不能同时接收鼠标事件了。解决这个问题的办法就是将进度条更新放在一个工作线程内,当两个线程同时工作,当然就可以同一时间做两件事了。

在工作线程中更新ProgressBar

更改代码如下:

private void button1_Click(object sender, EventArgs e)
{
    Thread t = new Thread(DoSomeWork);
    t.Start();
}
void DoSomeWork()
{
    int percent = 0;
    while (percent <= 100)
    {   //在线程中访问ProgressBar控件
        progressBar1.Value = percent;
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        percent += 5;
    }
}

运行程序,单击按钮,结果抛出异常,如下图所示:

我们不能在工作线程上访问主线程上创建的UI控件。要解决这个问题,可以把访问UI这件事包装在委托内,然后由窗体调用这个委托。也就是由窗体自己去更新UI控件,这样就没问题了。

使用委托方式在工作线程中更新ProgressBar

更新代码如下:

private void button1_Click(object sender, EventArgs e)
{
    Thread t = new Thread(DoSomeWork);
    t.Start();
}
//委托
delegate void SetProgressBarDelegate(int percent);
//委托所对应的方法
void SetProgressBar(int percent) 
{
    progressBar1.Value = percent;
}
void DoSomeWork()
{
    int percent = 0;
    while (percent <= 100)
    {
        this.BeginInvoke(new SetProgressBarDelegate(SetProgressBar), percent);
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        percent += 5;
    }
}

运行程序,在进度条更新时移动窗口就没有任何问题了。当然根据《Lambda表达式 - 委托进化史》这篇文章所学的lambda表达式知识,我们可以把程序写得更简单些:

private void button1_Click(object sender, EventArgs e)
{
    Thread t = new Thread(DoSomeWork);
    t.Start();
}
void DoSomeWork()
{
    int percent = 0;
    while (percent <= 100)
    {
        this.BeginInvoke(new Action<int>(p =>
            progressBar1.Value = percent
        ), percent);
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        percent += 5;
    }
}

这个程序是有bug的,当你在进度条更新期间关闭窗体,会引发一个异常。我们会在本文稍后部分解决这个问题。

向线程池中放入异步操作

以下所有程序都可以在Visual Studio Code内进行调试运行,继续使用Visual Studio也没有问题。运行以下代码:

static void Main(string[] args)
{
    const int x = 1;
    const int y = 2;
    const string lambdaState = "lambda state 2";

    ThreadPool.QueueUserWorkItem(AsyncOperation);
    Thread.Sleep(TimeSpan.FromSeconds(1));

    ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
    Thread.Sleep(TimeSpan.FromSeconds(1));

    ThreadPool.QueueUserWorkItem( state => {
            Console.WriteLine("Operation state: {0}", state);
            Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }, "lambda state");

    ThreadPool.QueueUserWorkItem( _ =>
    {
        Console.WriteLine("Operation state: {0}, {1}", x+y, lambdaState);
        Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(TimeSpan.FromSeconds(2));
    });

    Thread.Sleep(TimeSpan.FromSeconds(2));
}

private static void AsyncOperation(object state)
{
    Console.WriteLine("Operation state: {0}", state ?? "(null)");
    Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(TimeSpan.FromSeconds(2));
}

运行结果如下:

Operation state: (null)
Worker thread id: 3
Operation state: async state
Worker thread id: 4
Operation state: lambda state
Worker thread id: 5
Operation state: 3, lambda state 2
Worker thread id: 6

此例演示了QueueUserWorkItem的四种使用方法。其中state ?? "(null)"这句代码表示:当state的值为空时返回??右边的值。??叫空合并运算符。

首先定义了AsyncOperation方法,其接受单个object类型的参数。然后使用QueueUserWorkItem方法将该方法放到线程池中。接着再次放入该方法,但是这次给方法调用传入了一个状态对象。该对象将作为状态参数传递给AsynchronousOperation方法。

在操作完成后让线程睡眠一秒钟,从而让线程池拥有为新操作重用线程的可能性。如果注释掉所有的Thread.Sleep调用,那么所有打印出的线程ID多半是不一样的。如果ID是一样的,那很可能是前两个线程被重用来运行接下来的两个操作。

首先将一个lambad表达式放置到线程池中。这里没什么特别的。我们使用了lambda表达式语法,从而无须定义一个单独的方法。lambad表达式语法刚开始看很难受,多看,多做,慢慢习惯就好了。

然后,我们使用闭包机制,从而无须传递lambda表达式的状态。闭包更灵活,允许我们向异步操作传递一个以上的对象而且这些对象具有静态类型。所以之前介绍的传递对象给方法回调的机制即冗余又过时。在C#中有了闭包后就不再需要使用它了。

下来面解释最后一次异步操作中的箭头表达式前面的_符号。QueueUserWorkItem的函数原型是:

public static bool QueueUserWorkItem(WaitCallback callBack);

继续往下挖,WaitCallback的函数原型为:

public delegate void WaitCallback(object state);

也就是说QueueUserWorkItem里面使用的lambda表达式必须带一个object类型的参数。而如果我现在不想使用这个参数,那么就在箭头表达式前面用_符号来代替这个参数。

线程池与并行度

本节将展示线程池如何工作于大量的异步操作,以及它与创建大量单独的线程的方式有何不同。

运行以下代码:

using System;
using System.Threading;
using System.Diagnostics;

namespace Sync
{
    class Program
    {
        static void UseThreads(int numberOfOperations)
        {
            using (var countdown = new CountdownEvent(numberOfOperations))
            {
                Console.WriteLine("创建线程:");
                for (int i = 0; i < numberOfOperations; i++)
                {   //创建200条线程工作
                    var thread = new Thread(() =>
                      {
                          Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
                          Thread.Sleep(TimeSpan.FromSeconds(0.1));
                          countdown.Signal();
                      });
                    thread.Start();
                }
                countdown.Wait();//等待200个信号完成
                Console.WriteLine();
            }
        }

        static void UseThreadPool(int numberOfOperations)
        {
            using (var countdown = new CountdownEvent(numberOfOperations))
            {
                Console.WriteLine("使用线程池:");
                for (int i = 0; i < numberOfOperations; i++)
                {   //通过调用线程池的线程来工作
                    ThreadPool.QueueUserWorkItem(_ =>
                    {
                        Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(TimeSpan.FromSeconds(0.1));
                        countdown.Signal();
                    });
                }
                countdown.Wait();//等待200个信号完成
                Console.WriteLine();
            }
        }

        static void Main(string[] args)
        {
            const int numberOfOperations = 200;
            var sw = new Stopwatch();
            sw.Start();//计时开始
            UseThreads(numberOfOperations);
            sw.Stop();//计时结束
            Console.WriteLine("花费时间 : {0}", sw.ElapsedMilliseconds);

            sw.Reset();//重新计时
            sw.Start();//计时开始
            UseThreadPool(numberOfOperations);
            sw.Stop();//计时结束
            Console.WriteLine("花费时间 : {0}", sw.ElapsedMilliseconds);
        }
    }
}

运行结果:

当主线程启动时,创建了很多不同的线程,每个线程都运行一个操作。该操作打印出线程ID并阻塞线程100毫秒。结果我们创建了200条线程,全部并行运行这些操作。虽然在我的机器上总耗时是1235毫秒,但是所有线程消耗了大量的操作系统资源。

然后我们使用了执行同样的任务,只不过不为每个操作创建一个线程,而将它们放入到线程池中。然后线程池开始执行这些操作。从结果看一共只创建了5条线程,但花费了更多的时间,在我的机器上是4574毫秒。我们为操作系统节省了内存和线程数,但是为此付出了更长的执行时间。

实现一个取消选项

之前我们讨论过使用Thread.Abort方法终止线程是非常危险的,这一节演示如何使用正确方法来终止线程。

static void AsyncOperation1(CancellationToken token)
{
    Console.WriteLine("开始第一个任务");
    for (int i = 0; i < 5; i++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("第一个任务已经取消.");
            return;
        }
        Thread.Sleep(TimeSpan.FromSeconds(1));
    }
    Console.WriteLine("第一个任务已经成功完成");
}

static void AsyncOperation2(CancellationToken token)
{
    try
    {
        Console.WriteLine("开始第二个任务");
        for (int i = 0; i < 5; i++)
        {
            token.ThrowIfCancellationRequested();
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        Console.WriteLine("第二个任务已经成功完成");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("第二个任务已经取消.");
    }
}

static void AsyncOperation3(CancellationToken token)
{
    bool cancellationFlag = false;
    token.Register(() => cancellationFlag = true);
    Console.WriteLine("开始第三个任务");
    for (int i = 0; i < 5; i++)
    {
        if (cancellationFlag)
        {
            Console.WriteLine("第三个任务已被取消.");
            return;
        }
        Thread.Sleep(TimeSpan.FromSeconds(1));
    }
    Console.WriteLine("第三个任务已经成功完成");
}

static void Main(string[] args)
{
    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token));
        Thread.Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel(); //发送取消信号
    }

    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token));
        Thread.Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel();
    }

    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token));
        Thread.Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel();
    }

    Thread.Sleep(TimeSpan.FromSeconds(2));
}

简而言之,正确终止线程的方法应该发送一个取消信号,在线程中每做一个操作之前都判断是否收到取消信号,如果收到,则自行关闭。

本节中介绍了CancellationTokenSourceCancellationToken两个新类。它们在.NET4.0被引入,目前是实现异步操作的取消操作的事实标准。由于线程池已经存在了很长时间,并没有特殊的API来实现取消标记功能,但是仍然可以对线程池使用上述API。

在本程序中使用了三种方式来实现取消过程。第一个是来检查CancellationToken.IsCancellationRequested属性。如果该属性为true,则说明操作需要被取消,我们必须放弃该操作。

第二种方式是抛出一个OperationCanceledException异常。这允许在操作之外控制取消过程,即需要取消操作时,通过操作之外的代码来处理。

最后一种方式是注册一个回调函数。当操作被取消时,在线程池将调用该回调函数。这允许链式传递一个取消逻辑到另一个异步操作中。

修复进度条更新程序bug

在之前【多线程访问UI】这一节中,我们做更新进度条例子时留了个尾巴。当你在更新进度条期间如果关闭程序,会引发异常。如下图所示:

窗体已经被释放,还试图调用窗体的方法,自然不行。

现在我们学会了如何取消线程,正好拿来练练手。要解决这个问题,可以在窗体关闭事件里发信号,让线程自行结束。生成窗体的FormClosing事件,然后将程序代码更改如下:

CancellationTokenSource cts;
private void button1_Click(object sender, EventArgs e)
{
    cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    Thread t = new Thread(() => DoSomeWork(token));
    t.Start();
}
void DoSomeWork(CancellationToken token)
{
    int percent = 0;
    while (percent <= 100)
    {
        if (token.IsCancellationRequested)
        {
            return;
        }
        this.BeginInvoke(new Action<int>(p =>
            progressBar1.Value = percent
        ), percent);
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        percent += 5;
    }
    cts.Dispose();
    cts = null;
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (cts != null)
    {
        cts.Cancel();
    }
}

现在在更新进度条时关闭窗体,就没有问题了。

在线程池中使用等待事件处理器及超时

运行以下代码:

static void Main(string[] args)
{
    RunOperations(TimeSpan.FromSeconds(5));
    RunOperations(TimeSpan.FromSeconds(7));
}

static void RunOperations(TimeSpan workerOperationTimeout)
{
    using (var evt = new ManualResetEvent(false))
    using (var cts = new CancellationTokenSource())
    {
        Console.WriteLine("注册超时操作...");
        var worker = ThreadPool.RegisterWaitForSingleObject(evt,
            (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut), null, workerOperationTimeout, true);

        Console.WriteLine("开始长时间操作...");

        ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));

        Thread.Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
        worker.Unregister(evt);
    }
}

static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
{
    for(int i = 0; i < 6; i++)
    {
        if (token.IsCancellationRequested)
        {
            return;
        }
        Thread.Sleep(TimeSpan.FromSeconds(1));
    }
    evt.Set();
}

static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
{
    if (isTimedOut)
    {
        cts.Cancel();
        Console.WriteLine("超时并取消.");
    }
    else
    {
        Console.WriteLine("成功完成.");
    }
}

运行结果:

注册超时操作...
开始长时间操作...
超时并取消.
注册超时操作...
开始长时间操作...
成功完成.

线程池还有一个有用的方法:ThreadPool.RegisterWaitForSingleObject。该方法允许我们将回调函数放入线程池中的队列中。当提供的等待事件处理器收到信号或发生超时时,该回调函数将被调用。这允许我们为线程池中的操作实现超时功能。

RegisterWaitForSingleObject的原型为:

public static RegisteredWaitHandle RegisterWaitForSingleObject
(
    WaitHandle waitObject, 
    WaitOrTimerCallback callBack, 
    object state, 
    TimeSpan timeout, 
    bool executeOnlyOnce
);

参数解析:

public delegate void WaitOrTimerCallback(object state, bool timedOut);

此例中的WorkerOperationWait方法就是按照此委托打造的。其中的第一个参数用于取消线程。

当有大量的线程必须处于阻塞状态中等待一些多线程事件发信号时,以上方式非常有用。借助于线程池的基础设施,我们无需阻塞所有这样的线程。可以释放这些线程直到信号事件被设置。在服务器端应用程序中这是个非常重要的应用场景,因为服务器端应用程序要求高伸缩性及高性能。

使用计时器

本节将描述如何使用System.Threading.Timer对象来在线程池中创建周期性调用的异步操作。使用如下代码:

using System;
using System.Threading;
using System.Diagnostics;

namespace Sync
{
    class Program
    {
        static Timer _timer;
        static void Main(string[] args)
        {
            Console.WriteLine("输入 'Enter' 键来停止计时器...");
            DateTime start = DateTime.Now;
            _timer = new Timer(_ => TimerOperation(start),
                null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
            Thread.Sleep(TimeSpan.FromSeconds(6));
            _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));
            Console.ReadLine();
            _timer.Dispose();
        }

        static void TimerOperation(DateTime start)
        {
            TimeSpan elapsed = DateTime.Now - start;
            Console.WriteLine("运行时间:{0} 秒,线程ID {1}", elapsed.Seconds,
                Thread.CurrentThread.ManagedThreadId);
        }
    }
}

如果在vscode内运行此程序不会自动停止,需手动关闭。如果希望按回车键结束程序,请参考《线程同步》这篇文章中的【使用Mutex类】这一节来生成.exe文件运行。

运行结果:

输入 'Enter' 键来停止计时器...
运行时间:1 秒,线程ID 4
运行时间:3 秒,线程ID 4
运行时间:5 秒,线程ID 4
运行时间:7 秒,线程ID 4
运行时间:11 秒,线程ID 4
运行时间:15 秒,线程ID 4
运行时间:19 秒,线程ID 4
运行时间:23 秒,线程ID 4

首先我们看看计时器System.Threading.Timer的构造函数原型:

public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);

参数解析:

public delegate void TimerCallback(object state);

此委托需要一个object对象作为参数。此例中的TimerOperation方法即为此委托的实现。表示计时器每隔指定周期所要执行的方法。

我们首先创建了一个Timer实例。第一个参数量个lambda表达式,将会 在线程池中被执行。之后等待6秒后修改计时器。在调用_timer.Change方法一秒后启动TimerOperation,然后每隔4秒再次运行。

计时器还可以更复杂
可以以更复杂的方式使用计时器。比如,可以通过Timeout.Infinet值提供给计时器一个间隔参数来只允许计时器操作一次。然后在计时器异步操作内,能够设置下一次计时器操作将被执行的时间。具体时间取决于自定义业务逻辑。

使用BackgroundWorker组件

本节实例演示了另一种异步编程的方式,即使用BackgroundWorker组件。借助于该对象,可以将异步代码组织为一系列事件及事件处理器。你将学会如何使用该组件进行异步编程。

使用以下代码:

using System;
using System.Threading;
using System.ComponentModel;

namespace Sync
{
    class Program
    {
        static void Main(string[] args)
        {
            var bw = new BackgroundWorker();
            bw.WorkerReportsProgress = true;
            bw.WorkerSupportsCancellation = true;

            bw.DoWork += Worker_DoWork;
            bw.ProgressChanged += Worker_ProgressChanged;
            bw.RunWorkerCompleted += Worker_Completed;

            bw.RunWorkerAsync();

            Console.WriteLine("按 c 以取消工作");
            do
            {
                if (Console.ReadKey(true).KeyChar == 'c')
                {
                    bw.CancelAsync();
                }
            }
            while (bw.IsBusy);
        }

        static void Worker_DoWork(object sender, DoWorkEventArgs e)
        {
            Console.WriteLine("线程池中的线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
            var bw = (BackgroundWorker)sender;
            for (int i = 1; i <= 100; i++)
            {

                if (bw.CancellationPending)
                {
                    e.Cancel = true;
                    return;
                }

                if (i % 10 == 0)
                {
                    bw.ReportProgress(i);
                }

                Thread.Sleep(TimeSpan.FromSeconds(0.1));
            }
            e.Result = 42;
        }

        static void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            Console.WriteLine("完成{0}% , 线程池中的线程ID: {1}", e.ProgressPercentage,
                Thread.CurrentThread.ManagedThreadId);
        }

        static void Worker_Completed(object sender, RunWorkerCompletedEventArgs e)
        {
            Console.WriteLine("完成的线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
            if (e.Error != null)
            {
                Console.WriteLine("发生异常: {0}", e.Error.Message);
            }
            else if (e.Cancelled)
            {
                Console.WriteLine("操作被取消");
            }
            else
            {
                Console.WriteLine("结果是: {0}", e.Result);
            }
        }
    }
}

要想获取完整演示效果,请参考《线程同步》这篇文章中的【使用Mutex类】这一节来生成.exe文件运行。否则无法按c键中断程序。

运行结果:

按 c 以取消工作
线程池中的线程ID: 3
完成10% , 线程池中的线程ID: 4
完成20% , 线程池中的线程ID: 5
完成30% , 线程池中的线程ID: 5
完成40% , 线程池中的线程ID: 5
完成50% , 线程池中的线程ID: 5
完成60% , 线程池中的线程ID: 4
完成70% , 线程池中的线程ID: 5
完成80% , 线程池中的线程ID: 4
完成90% , 线程池中的线程ID: 5
完成100% , 线程池中的线程ID: 4
完成的线程ID: 5
结果是: 42

可以运行过程中按下c键取消工作。

BackgroundWorker使用的是事件机制来实现异步编程,这种方式被称为基于事件的异步模式(Event-based Asynchronous Pattern,简称EAP)。这是历史上第二种用来构造异步程序的方式,现在更推荐使用TPL,会在后面的文章中描述。

关于事件,可以参考我的《C#语言参考视频》,里面有详细讲解,这里不再讨论。本例实现了BackgroundWorker的三个事件:

可以线程运行的过程中使用 BackgroundWorker.CancelAsync() 方法发信号终止线程,在线程中通过 BackgroundWorker.CancellationPending 来判断是否收到此信号,从而决定是否取消线程。

BackgroundWorker组件实际上被用于Windows窗体应用程序(Windows Forms Applications,简称WPF)中。该实现通过后台工作事件处理器的代码可以直接与UI控制器交互。与线程池中的线程与UI控制器交互的方式相比较,使用BackgroundWorker组件的方式更加自然和好用。